package com.partsoft.dits;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.net.URI;
import java.util.Iterator;
import java.util.Properties;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.SAXReader;
import org.dom4j.io.XMLWriter;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.ContextStoppedEvent;
import org.springframework.context.support.FileSystemXmlApplicationContext;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import com.partsoft.dits.utils.Dom4jUtils;

/**
 * Spring上下文配置文件类
 */
public abstract class ContextConfigurableBean implements ConfigurableBean {

	private String contextProfileLocation = null;

	private boolean createNoExists = true;

	private boolean profileCanBeWrite = true;

	private ResourceLoader resourceLoader = new PathMatchingResourcePatternResolver();

	private Document contextDocument = null;

	private ApplicationContext ownerContext = null;

	public ContextConfigurableBean() {
	}

	public ContextConfigurableBean(String contextConfigLocation) {
		setContextProfileLocation(contextConfigLocation);
	}

	public ContextConfigurableBean(String contextConfigLocation, boolean createNoExists) {
		setContextProfileLocation(contextConfigLocation);
		setCreateProfileNoExists(createNoExists);
	}

	public void setOwnerContext(ApplicationContext ownerContext) {
		this.ownerContext = ownerContext;
	}

	public ApplicationContext getOwnerContext() {
		return ownerContext;
	}

	/**
	 * 是否允许写入
	 */
	public boolean isProfileCanBeWrite() {
		return profileCanBeWrite;
	}

	public void setProfileCanBeWrite(boolean profileCanBeWrite) {
		assertNoInit();
		this.profileCanBeWrite = profileCanBeWrite;
		if (!isProfileCanBeWrite())
			setCreateProfileNoExists(false);
	}

	/**
	 * 如果不存在则创建
	 */
	public boolean isCreateProfileNoExists() {
		return createNoExists;
	}

	public void setCreateProfileNoExists(boolean createNoExists) {
		assertNoInit();
		this.createNoExists = createNoExists && isProfileCanBeWrite();
	}

	public ResourceLoader getResourceLoader() {
		return resourceLoader;
	}

	public void setResourceLoader(ResourceLoader resourceLoader) {
		assertNoInit();
		this.resourceLoader = resourceLoader;
	}

	public String getContextProfileLocation() {
		return contextProfileLocation;
	}

	/**
	 * 如果设置文件系统中的文件前面需要加前缀: file:/
	 * 
	 * @param contextConfigLocation
	 *            配置文件路径
	 */
	public void setContextProfileLocation(String contextConfigLocation) {
		assertNoInit();
		this.contextProfileLocation = contextConfigLocation != null ? contextConfigLocation.replaceAll(" ", "%20")
				: contextConfigLocation;
	}

	protected File getContextProfile() throws IOException {
		String fname = getContextProfileLocation();
		try {
			Resource drs = getResourceLoader().getResource(fname);
			File f = drs.getFile();
			if (!f.exists()) {
				if (isCreateProfileNoExists()) {
					URI parentDir = f.getParentFile().toURI();
					FileUtils.forceMkdir(new File(parentDir));
					saveContextDocument(newSpringDocument(), f);
				} else {
					throw new IOException(String.format("not found config file (%s)", f.getCanonicalPath()));
				}
			} else if (f.isDirectory()) {
				throw new IOException(String.format("file name (%s) is directory", f.getCanonicalPath()));
			}
			return f;
		} catch (Throwable e) {
			throw new IOException(String.format("error config file path (%s)", fname), e);
		}
	}

	public void finiContext() {
		synchronized (this) {
			unLoadContextConfiguredBeans();
			contextDocument = null;
		}
	}

	public boolean isAlreadyInit() {
		return contextDocument != null;
	}

	protected void assertNoInit() {
		Assert.isTrue(!isAlreadyInit(), "context already initialized.");
	}

	protected void assertAlreadyInit() {
		Assert.isTrue(isAlreadyInit(), "context uninitialzed");
	}

	public void initContext() {
		synchronized (this) {
			if (isAlreadyInit())
				finiContext();
			try {
				File f = getContextProfile();
				FileSystemXmlApplicationContext ctx = new FileSystemXmlApplicationContext(
						new String[] { f.getCanonicalPath() }, false, getOwnerContext());
				try {
					ctx.refresh();
				} catch (RuntimeException e) {
					throw new Exception(String.format("failed load context(%s)", e.getMessage()), e);
				}
				Document tempDoc = loadSpringDocument(f);
				if (isProfileCanBeWrite()) {
					saveContextDocument(tempDoc, f);
				}
				loadContextConfiguredBeans(ctx);
				contextDocument = tempDoc;
				ctx.close();
			} catch (Throwable e) {
				throw new IllegalArgumentException(String.format("failed init context:%s", e.getMessage()), e);
			}
		}
	}

	protected Document getContextDocument() {
		assertAlreadyInit();
		return contextDocument;
	}

	protected abstract void loadContextConfiguredBeans(ConfigurableApplicationContext ctx);

	protected abstract void unLoadContextConfiguredBeans();

	public void setApplicationContext(ApplicationContext ownerContext) throws BeansException {
		setOwnerContext(ownerContext);
	}

	public void onApplicationEvent(ApplicationEvent event) {
		if (getOwnerContext() == null)
			return;
		if (!ObjectUtils.nullSafeEquals(getOwnerContext(), event.getSource()))
			return;
		if (event instanceof ContextRefreshedEvent) {
			initContext();
		} else if (event instanceof ContextStoppedEvent) {
			finiContext();
		}
	}

	public void appendConfiguredBean(String id, String className, Properties props) throws IOException {
		Assert.notNull(props);
		appendBean(getContextDocument(), id, className, props);
		saveContextDocument(getContextDocument(), getContextProfile());
	}

	public void removeConfiguredBean(String id) throws IOException {
		removeBean(getContextDocument(), id);
		saveContextDocument(getContextDocument(), getContextProfile());
	}

	public static void saveContextDocument(Document doc, File file) throws IOException {
		Assert.notNull(doc);
		Assert.notNull(file);
		OutputFormat format = OutputFormat.createPrettyPrint();
		format.setEncoding(doc.getXMLEncoding());
		Writer fwr = new FileWriter(file);
		try {
			XMLWriter writer = new XMLWriter(fwr, format);
			writer.write(doc);
			writer.close();
		} catch (IOException e) {
			throw new IOException(String.format("failed write config file (%s): %s", e.getMessage()), e);
		} finally {
			IOUtils.closeQuietly(fwr);
		}
	}

	public static Document newSpringDocument() throws Exception {
		Document doc = DocumentHelper.parseText("<?xml version=\"1.0\" encoding=\"utf-8\"?>" + "<beans"
				+ "	 xmlns=\"http://www.springframework.org/schema/beans\""
				+ "	 xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""
				+ "	 xsi:schemaLocation=\"http://www.springframework.org/schema/beans"
				+ "           http://www.springframework.org/schema/beans/spring-beans.xsd\">" + "</beans>");
		return doc;
	}

	public static Document loadSpringDocument(File f) throws IOException {
		InputStream stream = new FileInputStream(f);
		try {
			SAXReader reader = new SAXReader();
			return reader.read(stream);
		} catch (Throwable e) {
			throw new IOException(String.format("failed read config file (%s): %s", f.getCanonicalPath(),
					e.getMessage()), e);
		} finally {
			IOUtils.closeQuietly(stream);
		}
	}

	public static Element appendBean(Document doc, String id, String className, Properties props) {
		Assert.notNull(doc);
		Assert.hasText(className);
		className = className.trim();
		Element root = doc.getRootElement();
		Element bean = root.addElement("bean");
		if (StringUtils.hasText(id)) {
			bean.addAttribute("id", Dom4jUtils.encodeXML(id));
		}
		bean.addAttribute("class", className);
		for (Object key : props.keySet()) {
			Element p = bean.addElement("property");
			p.addAttribute("name", key.toString());
			p.addAttribute("value", props.getProperty(key.toString()));
		}
		return bean;
	}

	public static Document removeBean(Document doc, String beanId) {
		Assert.notNull(beanId);
		Element beans = doc.getRootElement();
		Element bean = null;
		for (Iterator<?> iterator = beans.elementIterator(); iterator.hasNext();) {
			Element b = (Element) iterator.next();
			if (beanId.equals(b.attributeValue("id"))) {
				bean = b;
				break;
			}
		}

		if (bean != null) {
			beans.remove(bean);
		}
		return doc;
	}

}
